#!/usr/bin/env ruby

require 'nokogiri'
require 'json'

class CameraSupportState
  SOURCE_FILES={
    :cameras => "src/external/rawspeed/data/cameras.xml",
    :coeffs => "src/external/adobe_coeff.c",
    :wbpresets => "src/external/wb_presets.c",
    :colormatrices => "src/common/colormatrices.c",
    :noiseprofiles => "data/noiseprofiles.json",
  }
  PANASONIC_NEEDED_FORMATS=["4:3", "3:2", "16:9", "1:1"]

  def initialize(opts={})
    @samplesdir = opts[:samplesdir]
    @basedir=Dir.pwd
    opts[:ref] ||= 'HEAD'
    @submodules=get_submodules(opts[:ref])
    @sources = Hash[
      SOURCE_FILES.map do |name, file|
        [name, file_content(file, opts[:ref])]
      end
    ]
    process_files
  end

  def file_content(filename, rev)
    submodule,submodulerev=get_submodule(filename)
    goodname = filename.dup
    if submodule
      Dir.chdir(submodule)
      goodname.gsub!("#{submodule}/",'')
      rev=submodulerev
    end
    ret=IO.popen("git show #{rev}:#{goodname}")
    if submodule
      Dir.chdir(@basedir)
    end
    ret
  end

  def get_submodules(rev)
    ret={}
    IO.popen("git ls-tree --full-tree -z -r #{rev}") do |fh|
      fh.each_line("\0") do |line|
        # 100644 blob bc16048078e48ccfa6a1aaa539fa295e7d9f2601    tools/wb_presets_common.rb
        mo=/\A(?<mode>\d+) (?<type>\S+) (?<hash>\S+)\t(?<path>\S+)\0\z/.match(line)
        if mo and (mo[:type] == 'commit')
          ret[mo[:path]]=mo[:hash]
        end
      end
    end
    ret
  end

  def get_submodule(filename)
    submodule=@submodules.select {|dir,hash| filename.start_with?(dir) }.first
  end

  def get_maker_model(file)
    def get_exif_key(key, file)
      f = IO.popen("exiv2 -g \"#{key}\" -Pt \"#{file}\" 2>/dev/null","r")
      c = f.read
      f.close
      return c
    end

    maker = get_exif_key("Exif.Image.Make", file)
    maker = maker[0..6] == "SAMSUNG" ? "SAMSUNG" : maker.strip
    model = get_exif_key("Exif.Image.Model", file)
    model = model[0..5] == "NX2000" ? "NX2000" : model.strip

    if (maker == "" || model == "") # Try with rawspeed instead
      f = IO.popen("/opt/darktable/bin/darktable-rs-identify \"#{file}\"","r")
      f.each do |line|
        parts = line.split(":")
        case parts[0].strip
        when "make"
          maker = parts[1..-1].join(":").strip
        when "model"
          model = parts[1..-1].join(":").strip
        end
      end
      f.close
    end
    return [maker,model]
  end

  def process_files
    @rawspeed_cameras = {}
    @rawspeed_aliases = {}
    @rawspeed_panasonic_formats = {}
    @rawspeed_dngs = {}
    @rawspeed_modes = {}
    @exif_name_map = {}
    @exif_alias_map = {}
    xml_doc  = Nokogiri::XML(@sources[:cameras])
    xml_doc.css("Camera").each do |c|
      maker = exif_maker = c.attribute("make").value
      model = c.attribute("model").value
      exif_id = "#{maker} #{model}"
      if c.css("ID")[0]
        maker = c.css("ID")[0].attribute("make").value
        model = c.css("ID")[0].attribute("model").value
      end
      id = "#{maker} #{model}"
      supported = !c.attribute("supported") || c.attribute("supported").value == "yes"
      if supported
        @rawspeed_cameras[id] = 0
        @rawspeed_aliases[id] = [id]
        @exif_name_map[exif_id] = id
        @exif_alias_map[exif_id] = id
        mode = ""
        mode = c.attribute("mode").value if c.attribute("mode")
        if mode != ""
          @rawspeed_modes[id] ||= []
          @rawspeed_modes[id] <<= mode
        end
        @rawspeed_dngs[id] = true if mode == "dng"
        if PANASONIC_NEEDED_FORMATS.include?(mode)
          @rawspeed_panasonic_formats[id] ||= []
          @rawspeed_panasonic_formats[id] << mode
        end
        c.css("Alias").each do |a|
          exif_model = model = a.content
          exif_id = "#{exif_maker} #{exif_model}"
          model = a.attribute("id").value if a.attribute("id")
          aliasid = "#{maker} #{model}"
          @rawspeed_aliases[id] << aliasid if aliasid != id
          @exif_name_map[exif_id] = id
          @exif_alias_map[exif_id] = aliasid
          if mode != ""
            @rawspeed_modes[aliasid] ||= []
            @rawspeed_modes[aliasid] <<= mode
          end
        end
      end
    end
    @rawspeed_cameras = @rawspeed_cameras.keys

    @coeffs_cameras = {}
    @sources[:coeffs].each do |line|
      if line[0..4] == "    {"
        @coeffs_cameras[line.split('"')[1]] = 0
      end
    end

    @presets_cameras = {}
    @sources[:wbpresets].each do |line|
      if line[0..2] == "  {"
        lineparts = line.split('"')
        @presets_cameras["#{lineparts[1]} #{lineparts[3]}"] = 0
      end
    end

    @colormatrices_cameras = {}
    @sources[:colormatrices].each do |line|
      if line[0..2] == "  {"
        @colormatrices_cameras[line.split('"')[1]] = 0
      end
    end

    @noiseprofiles_cameras = {}
    JSON.parse(@sources[:noiseprofiles].read)['noiseprofiles'].each do |mak|
      maker = mak['maker']
      mak['models'].each do |mod|
        model = mod['model']
        @noiseprofiles_cameras["#{maker} #{model}"] = 0
      end
    end

    @samples_cameras = {}
    @samples_alias_cameras = {}
    if @samplesdir
      Dir["#{@samplesdir}/**/**/*"].each do |file|
        if (File.file?(file))
          maker, model = get_maker_model(file)
          if (maker != "" && model != "")
            id = name = alias_name = "#{maker} #{model}"
            name = @exif_name_map[id] if @exif_name_map[id]
            alias_name = @exif_alias_map[id] if @exif_alias_map[id]
            @samples_cameras[name] = 0
            @samples_alias_cameras[alias_name] = 0
          end
        end
      end
    end
  end

  def compare_lists(name, cameras, db, verbose_miss, verbose_nomatch)
    miss_cams = []
    cameras.each do |c|
      if !db[c]
        miss_cams << c
      else
        db[c] += 1
      end
    end

    miss_db = []
    db.each do |c, num|
      if num == 0
        miss_db << c
      end
    end

    puts "For #{name} found #{miss_cams.size} cameras missing and #{miss_db.size} entries for no cam"
    miss_cams.each {|c| puts "  MISS: #{c}"} if verbose_miss
    miss_db.each {|c| puts "  NOMATCH: #{c}"} if verbose_nomatch
  end

  def default_listing(verbose_mode, quiet_mode)
    puts "Found #{@rawspeed_cameras.size} cameras #{@coeffs_cameras.size} adobe_coeffs #{@presets_cameras.size} wb_coeffs #{@colormatrices_cameras.size} colormatrices #{@noiseprofiles_cameras.size} noise profiles #{@samples_cameras.size} samples"
    rawspeed_coeffs_cameras = @rawspeed_cameras.select{|id| !@rawspeed_dngs[id]}
    compare_lists("adobe_coeffs", @rawspeed_cameras, @coeffs_cameras, !quiet_mode, verbose_mode)
    compare_lists("wb_presets", @rawspeed_cameras, @presets_cameras, verbose_mode, !quiet_mode)
    compare_lists("colormatrices", @rawspeed_cameras, @colormatrices_cameras, verbose_mode, !quiet_mode)
    compare_lists("noiseprofiles", @rawspeed_cameras, @noiseprofiles_cameras, verbose_mode, !quiet_mode)
    compare_lists("samples", @rawspeed_cameras, @samples_cameras, verbose_mode, !quiet_mode) if @samplesdir
    @rawspeed_panasonic_formats.each do |camera, formats|
      if formats.size != 4
        missing_formats = PANASONIC_NEEDED_FORMATS.select{|f| !formats.include?(f)}
        puts "Missing formats for '#{camera}' #{missing_formats.join(' ')}"
      end
    end
  end

  def matrix_listing
    puts "<!-- valid as of #{Time.now.strftime("%Y/%m/%d")}, #{IO.popen("git describe").readlines[0]} -->"
    puts "<table class=\"smalltext altrows u-full-width\">"

    counts = [0, 0, 0, 0]
    clist = ""
    i = 1
    camera_list.each do |camera, al|
      counts[0] += 1
      i += 1
      clist << "  <tr>"
      clist << "<td>#{al}</td>"
      [@presets_cameras[camera],
       @noiseprofiles_cameras[camera], @colormatrices_cameras[camera]].each_with_index do |value, num|
        counts[num+1] += 1 if value
        value = value ? "Yes" : "<strong>NO</strong>"
        clist << "<td>#{value}</td>"
      end
      clist << "</tr>\n"
    end

    puts "  <thead><tr>"
    ["Camera", "WB Presets", "Noise Profile", "Custom Matrix"].each_with_index do |name, i|
      $stdout.write "    <th><strong>#{name}</strong><br/>"
      $stdout.write "#{counts[i]} (#{(counts[i].to_f/counts[0].to_f*100).to_i}%)"
      $stdout.write "</th>\n"
    end
    puts "  </tr></thead>"
    $stdout.write clist
    puts "</table>"
  end

  def camera_list
    list = []
    @rawspeed_cameras.sort.each do |camera|
      @rawspeed_aliases[camera].sort.each do |al|
        list << [camera, al]
      end
    end
    list
  end

  attr_reader :rawspeed_modes
  attr_reader :presets_cameras
  attr_reader :noiseprofiles_cameras
  attr_reader :colormatrices_cameras
  attr_reader :exif_alias_map

  def diff_listing(from)
    new_basics   = []
    new_presets  = []
    new_profiles = []
    new_matrices = []

    from_cameras = Hash[from.camera_list.map{|camera, al| [al, true]}]

    # Create a reverse map between alias and possible EXIF names
    alias_exif_map = {}
    @exif_alias_map.each do |exif, al|
      alias_exif_map[al] ||= []
      alias_exif_map[al] <<= exif
    end

    camera_list.each do |camera, al|
      fromals = alias_exif_map[al].map{|exif| from.exif_alias_map[exif]}
      fromals.delete nil
      if (!from_cameras[al] && # Camera was not listed before
         fromals.size == 0) || # And it wasn't just an ID rename
         from.rawspeed_modes[al] != @rawspeed_modes[al] # Or it has new modes
        modes = (@rawspeed_modes[al]||[]).dup
        (from.rawspeed_modes[al]||[]).each {|m| modes.delete m}
        new_basics << al+(modes.size>0 ? " ("+modes.join(", ")+")" : "")
      end

      def test_camera(to_cameras, cam, from_cameras, fromals)
        return false if !to_cameras[cam] # It doesn't exist in the new version
        return false if from_cameras[cam] # It already existed in the old version
        fromals.each{|fromal| return false if from_cameras[fromal]} # It wasn't just an ID rename
        return true
      end

      new_presets << al  if test_camera(@presets_cameras, camera, from.presets_cameras, fromals)
      new_profiles << al if test_camera(@noiseprofiles_cameras, camera, from.noiseprofiles_cameras, fromals)
      new_matrices << al if test_camera(@colormatrices_cameras, camera, from.colormatrices_cameras, fromals)
    end

    def print_list(name, list)
      return if list.size == 0
      puts name
      puts
      list.each do |item|
        puts "- "+item
      end
      puts
    end

    print_list "### Base Support", new_basics
    print_list "### White Balance Presets", new_presets
    print_list "### Noise Profiles", new_profiles
    print_list "### Custom Color matrices", new_matrices
  end
end #class CameraSupportState

if ARGV[0] == "--compare" && ARGV.size == 3
  from = CameraSupportState.new(:ref=>ARGV[1])
  to = CameraSupportState.new(:ref=>ARGV[2])
  to.diff_listing(from)
else
  matrix_mode = nil
  verbose_mode = false
  quiet_mode = false
  samplesdir = nil

  ARGV.each do |arg|
    if arg == "--matrix"
      matrix_mode = true
    elsif arg == "--verbose"
      verbose_mode = true
    elsif arg == "--quiet"
      quiet_mode = true
    elsif samplesdir == nil && File.exists?(arg)
      samplesdir = arg
    else
      $stderr.puts "Usage: check_camera_support [--verbose] [--matrix] <samples dir>"
      $stderr.puts "       check_camera_support --compare <from git> <to git>"
      exit 2
    end
  end

  css = CameraSupportState.new(:samplesdir => samplesdir)
  if matrix_mode
    css.matrix_listing
  else
    css.default_listing(verbose_mode, quiet_mode)
  end
end
